﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;
using Protocolo;
using System.Security.Cryptography;
using System.Configuration;
using System.Diagnostics;

namespace Servidor
{
    class Server
    {
        TcpListener Servidor;
        UdpClient UDPClient;
        private int CantThreads;
        public double[] Bandwidth;
        public double LocalBW { get; set; }
        int UDPPort;
        static List<string> ListaArchivosLocal = new List<string>();
        Dictionary<string, string> ListaServidores = new Dictionary<string, string>();
        Dictionary<string, double> ListaBW = new Dictionary<string, double>();

        public Server()
        {
            double MaxBw = Int32.Parse(ConfigurationManager.AppSettings["MaxBandwithTotal"]);
            double MaxBwCliente = Int32.Parse(ConfigurationManager.AppSettings["MaxBandwithClient"]);
            double CantClientes = Math.Ceiling(MaxBw / MaxBwCliente);
            try
            {
                CantThreads = Convert.ToInt32(CantClientes);
            }
            catch (OverflowException)
            {
                CantThreads = 10;
            }
            Bandwidth = new double[CantThreads];
            UDPPort = Int32.Parse(ConfigurationManager.AppSettings.GetValues("UdpPort")[0]);

            Int32 port = Int32.Parse(ConfigurationManager.AppSettings.GetValues("TcpPort")[0]);
            string IPLocal = ConfigurationManager.AppSettings.GetValues("IPLocal")[0];
            IPAddress localAddr = IPAddress.Parse(IPLocal);

            Servidor = new TcpListener(localAddr, port);

            Servidor.Start();

            Console.WriteLine("Server started.");

            Thread[] threadPool = new Thread[CantThreads];

            for (int i = 0; i < threadPool.Length; i++)
            {
                threadPool[i] = new Thread(DoWork);
                threadPool[i].Start(i);
            }

            UDPClient = new UdpClient(UDPPort);
            UDPClient.JoinMulticastGroup(IPAddress.Parse("224.100.0.1"), 50);
            Thread UDPThread = new Thread(StartUdpServer);
            UDPThread.Start();

            Thread BWThread = new Thread(InformBW);
            BWThread.Start();

            Thread FilesSyncThread = new Thread(SyncFiles);
            FilesSyncThread.Start();

            Console.WriteLine("\nHit enter to continue...");
            Console.Read();
            for (int i = 0; i < threadPool.Length; i++)
            {
                threadPool[i].Interrupt();
                Console.WriteLine("Thread " + i + " interrupted");
            }
        }

        private void DoWork(object o)
        {
            try
            {
                int thread = (int)o;

                // Buffer for reading data
                Byte[] bytes = new Byte[266];

                //Get folder for saving
                string Folder = ConfigurationManager.AppSettings.GetValues("Folder")[0];

                // Enter the listening loop.
                while (true)
                {
                    Console.Write("Waiting for a connection on thread: " + thread + "\n");

                    // Perform a blocking call to accept requests.
                    // You could also user server.AcceptSocket() here.
                    TcpClient client = Servidor.AcceptTcpClient();
                    Console.WriteLine("Connected!");

                    // Get a stream object for reading and writing
                    NetworkStream stream = client.GetStream();

                    //Wait for data on the stream
                    while (!stream.DataAvailable) { }

                    int BytesRead;
                    Stopwatch sw = new Stopwatch();
                    sw.Start();
                    //Read first package
                    BytesRead = stream.Read(bytes, 0, bytes.Length);

                    //Get command
                    PaqueteREQ received = new PaqueteREQ(bytes);
                    string commando = string.Empty;

                    commando = received.GetCmd().ToString();
                    string NombreArchivo;

                    switch (commando)
                    {
                        #region Case Upload
                        case "01":

                            //Get File name
                            NombreArchivo = received.GetName();

                            //Get MD5
                            string MD5 = received.getMD5();

                            // Tamaño Archivo

                            double tamañoArchivot = received.getTamañoArchivo();
                            //Get Cantidad de paquetes
                            int PaquetesTotal = received.GetCantidadPaquetes();

                            int PaquetesRecibidos = 0;
                            FileStream fs = File.Open(Folder + NombreArchivo, FileMode.Create, FileAccess.Write, FileShare.None);

                            byte[] msg;
                            msg = Encoding.ASCII.GetBytes("Se recibio el archivo.");
                            stream.Write(msg, 0, msg.Length);

                            // Loop to receive all the data sent by the client.
                            while (PaquetesRecibidos < PaquetesTotal - 1)
                            {
                                while (!stream.DataAvailable) ;
                                PaquetesRecibidos++;
                                if (tamañoArchivot < bytes.Length - 10)
                                    bytes = new byte[Convert.ToInt32(tamañoArchivot) + 10];

                                BytesRead = stream.Read(bytes, 0, bytes.Length);

                                //Calculates thread bandwith
                                if (sw.ElapsedMilliseconds == 0)
                                    Bandwidth[thread] = 0;
                                else
                                    Bandwidth[thread] = bytes.Length / sw.ElapsedMilliseconds;
                                received = new PaqueteREQUpload(bytes);
                                received.ByteArray = bytes;
                                commando = received.GetCmd().ToString();

                                // Add information to the file.
                                //TODO : mandar paquete correspondiente
                                if (bytes.Length < 266)
                                {
                                    byte[] aux = (byte[])bytes.Skip(10).Take(bytes.Length - 10).ToArray();
                                    fs.Write(aux, 0, aux.Length);
                                }
                                else
                                    fs.Write(received.GetDatos(), 0, received.GetDatos().Length);

                                msg = Encoding.ASCII.GetBytes("Se recibio el archivo.");
                                stream.Write(msg, 0, msg.Length);
                                tamañoArchivot -= received.GetDatos().Length;
                            }
                            //Resets thread bandwidth
                            Bandwidth[thread] = 0;
                            sw.Stop();
                            //Flush filestream and close file.
                            fs.Flush();
                            fs.Close();

                            //After Upload Check MD5
                            string md5 = string.Empty;
                            using (FileStream fss = File.OpenRead(Folder + NombreArchivo))
                            {
                                using (MD5CryptoServiceProvider m = new MD5CryptoServiceProvider())
                                {
                                    var md5a = m.ComputeHash(fss);
                                    md5 = BitConverter.ToString(md5a);
                                }
                            }

                            if (md5 == MD5)
                            {
                                Console.WriteLine("Recibido archivo " + NombreArchivo + " en el thread: " + thread + "\n");
                                msg = Encoding.ASCII.GetBytes("Se recibio el archivo.");
                                stream.Write(msg, 0, msg.Length);
                                ListaArchivosLocal.Add(NombreArchivo);
                            }
                            else
                            {
                                Console.WriteLine("Error integridad. No se Recibio archivo " + NombreArchivo + " en el thread: " + thread + "\n");
                                msg = Encoding.ASCII.GetBytes("Error de integridad MD5.");
                                stream.Write(msg, 0, msg.Length);
                                File.Delete(Folder + NombreArchivo);
                            }

                            // Shutdown and end connection
                            client.Close();
                            break;
                        #endregion

                        #region Case Download
                        case "00":

                            //Get File name
                            NombreArchivo = received.GetName();
                            int PaquetePartida = string.IsNullOrEmpty(received.getNumeroPaquete()) ? 0 : Int32.Parse(received.getNumeroPaquete());

                            long tamañoArchivo = 0;

                            using (FileStream fss = File.OpenRead(Folder + NombreArchivo))
                            {
                                tamañoArchivo = fss.Length;
                                using (MD5CryptoServiceProvider m = new MD5CryptoServiceProvider())
                                {
                                    var md5a = m.ComputeHash(fss);
                                    md5 = BitConverter.ToString(md5a);
                                }

                            }

                            double dCantPaquetes = Math.Ceiling((double)tamañoArchivo / 256);
                            int cantidadPaquetes = Convert.ToInt32(dCantPaquetes);

                            int contadorPaquetes = 0;
                            Byte[] dataBuffer = new Byte[5120];

                            bool primeraVez = true;

                            FileStream fas = File.OpenRead(Folder + NombreArchivo);

                            // Get a client stream for reading and writing.
                            stream = client.GetStream();
                            string FileName = string.Empty;

                            //la cantidad de paquetes ya enviados.
                            contadorPaquetes = PaquetePartida;

                            while (contadorPaquetes <= cantidadPaquetes)
                            {
                                fas.Read(dataBuffer, 0, dataBuffer.Length);

                                // Translate the passed file into ASCII and store it as a Byte array.
                                Byte[] data = dataBuffer;

                                var PaquetesEnviar = ProtocolRESDownload.CrearPaquetes(data, FileName, primeraVez, cantidadPaquetes, md5, tamañoArchivo);

                                bool entraFor = false;
                                for (int i = PaquetePartida; i < PaquetesEnviar.Length; i++)
                                {
                                    entraFor = true;
                                    if (contadorPaquetes > cantidadPaquetes) break;

                                    // Send the message to the connected TcpServer. 
                                    var item = PaquetesEnviar[i];
                                    sw.Restart();
                                    stream.Write(item.ByteArray, 0, item.ByteArray.Length);

                                    // Receive the TcpServer.response.
                                    // Buffer to store the response bytes.
                                    byte[] data2 = new Byte[256];

                                    // String to store the response ASCII representation.
                                    String responseData = String.Empty;

                                    // Read the first batch of the TcpServer response bytes.
                                    Int32 CantBytes = stream.Read(data2, 0, data2.Length);
                                    responseData = Encoding.ASCII.GetString(data2, 0, CantBytes);
                                    //Calculates thread bandwith
                                    if (sw.ElapsedMilliseconds != 0)
                                        Bandwidth[thread] = item.ByteArray.Length / sw.ElapsedMilliseconds;
                                    else
                                        Bandwidth[thread] = 0;

                                    contadorPaquetes++;
                                }

                                if (contadorPaquetes < cantidadPaquetes)
                                {
                                    if (!entraFor && primeraVez)
                                    {
                                        //Restamos y enviamos el primer paquete para el resume
                                        PaquetePartida = PaquetePartida - PaquetesEnviar.Length;

                                        // Send the message to the connected TcpServer. 
                                        var item2 = PaquetesEnviar[0];
                                        stream.Write(item2.ByteArray, 0, item2.ByteArray.Length);

                                        // Receive the TcpServer.response.
                                        // Buffer to store the response bytes.
                                        byte[] data2 = new Byte[256];

                                        // String to store the response ASCII representation.
                                        String responseData2 = String.Empty;

                                        // Read the first batch of the TcpServer response bytes. ACK
                                        Int32 CantBytes2 = stream.Read(data2, 0, data2.Length);
                                        responseData2 = Encoding.ASCII.GetString(data2, 0, CantBytes2);

                                    }
                                    else if (!entraFor)
                                    {

                                        PaquetePartida = PaquetePartida - PaquetesEnviar.Length;

                                    }
                                    else if (entraFor)
                                    {
                                        //Si estamos en un resume el Paquete partida para empezar a enviar queda en un numero random. 
                                        //Debe ser cero para el proximo paquete de paquetes.
                                        PaquetePartida = 0;
                                    }
                                }
                                primeraVez = false;
                            }
                            sw.Stop();
                            client.Close();
                            break;
                        #endregion
                    }
                }
            }
            catch (SocketException e)
            {
                Console.WriteLine("SocketException: {0}", e);
            }
            finally
            {
                // Stop listening for new clients.
                Servidor.Stop();
            }
        }

        public void StartUdpServer()
        {

            Console.WriteLine("Esperando multicast...");
            while (true)
            {
                var ipEndPoint = new IPEndPoint(IPAddress.Any, 0);
                byte[] data = UDPClient.Receive(ref ipEndPoint);
                PaqueteREQ paqudp = new PaqueteREQ(data);
                if (paqudp.GetCmd() == "03")
                {
                    string DatosAux = Encoding.ASCII.GetString(paqudp.GetDatos());
                    string RemoteIP = DatosAux.Split('@')[0];
                    string RemotePort = DatosAux.Split('@')[1];
                    string LocalIp = ConfigurationManager.AppSettings.GetValues("IPLocal")[0];

                    double localBW = 0;
                    try
                    {
                        localBW = ListaBW[LocalIp];
                    }
                    catch (Exception)
                    {

                        //Not loaded yet then 0.
                        localBW = 0;
                    }

                    bool response = true;
                    foreach (var item in ListaBW)
                    {
                        if (item.Value > localBW)
                        {
                            response = false;
                            break;
                        }
                    }

                    if (response)
                    {
                        string port = ConfigurationManager.AppSettings.GetValues("TcpPort")[0];

                        //Envio si soy el server con mayor ancho de banda
                        string IPLocal = ConfigurationManager.AppSettings.GetValues("IPLocal")[0];
                        Paquete paq = ProtocolRESUDP.CrearPaquetes(IPLocal, port);
                        byte[] resp = paq.ByteArray;
                        Console.WriteLine("Enviando respuesta");
                        UDPClient.Send(resp, resp.Length, RemoteIP, Int32.Parse(RemotePort));
                    }
                }

                if (paqudp.GetCmd() == "04")
                {
                    string DatosAux = Encoding.ASCII.GetString(paqudp.GetDatos());
                    string RemoteIP = DatosAux.Split('@')[0];
                    string RemotePort = DatosAux.Split('@')[1];
                    double RemoteBW = Double.Parse(DatosAux.Split('@')[2]);
                    lock (this)
                    {
                        if (ListaServidores.ContainsKey(RemoteIP))
                        {
                            ListaServidores[RemoteIP] = RemotePort;
                            ListaBW[RemoteIP] = RemoteBW;
                        }
                        else
                        {
                            ListaServidores.Add(RemoteIP, RemotePort);
                            ListaBW.Add(RemoteIP, RemoteBW);
                        }
                    }
                }

                if (paqudp.GetCmd() == "05")
                {
                    string DatosAux = Encoding.ASCII.GetString(paqudp.GetDatos());
                    string RemoteIP = DatosAux.Split('@')[0];
                    string RemotePort = DatosAux.Split('@')[1];
                    string RemoteFiles = DatosAux.Split('@')[2];

                    List<string> ListaArchivos = RemoteFiles.Split(',').ToList();
                    List<string> ArchivosBajar = ListaArchivos.Where(c => !(ListaArchivosLocal.Contains(c))).ToList();
                    if (ArchivosBajar.Count != 0)
                    {
                        foreach (string file in ArchivosBajar)
                        {
                            if (file != "")
                                DownloadFile(file, RemoteIP, RemotePort);
                        }
                    }
                }
            }
        }

        public static TcpClient Connect(string ip, int port)
        {

            TcpClient client = null;
            try
            {
                // Create a TcpClient.
                client = new TcpClient(ip, port);
            }
            catch (ArgumentNullException e)
            {
                Console.WriteLine("ArgumentNullException: {0}", e);
            }
            catch (SocketException e)
            {
                Console.WriteLine("SocketException: {0}", e);
            }

            return client;
        }

        public void InformBW()
        {
            int MaxBw = Int32.Parse(ConfigurationManager.AppSettings["MaxBandwithTotal"]);
            int UdpPort = Int32.Parse(ConfigurationManager.AppSettings["UdpPort"]);
            string TcpPort = ConfigurationManager.AppSettings["TcpPort"];
            Stopwatch sw = new Stopwatch();
            sw.Start();
            while (true)
            {
                foreach (var v in Bandwidth)
                {
                    LocalBW += v;
                }
                double AvailableBW = MaxBw - LocalBW;
                string IPLocal = ConfigurationManager.AppSettings.GetValues("IPLocal")[0];
                Paquete p = ProtocolUDPBandwidth.CrearPaquetes(IPLocal, TcpPort, AvailableBW.ToString());
                UDPClient.Send(p.ByteArray, p.ByteArray.Length, "224.100.0.1", UdpPort);
                while (sw.ElapsedMilliseconds < 60000) { }
                sw.Restart();
            }
        }

        public void SyncFiles()
        {
            int MaxBw = Int32.Parse(ConfigurationManager.AppSettings["MaxBandwithTotal"]);
            int UdpPort = Int32.Parse(ConfigurationManager.AppSettings["UdpPort"]);
            string TcpPort = ConfigurationManager.AppSettings["TcpPort"];
            Stopwatch sw = new Stopwatch();
            sw.Start();
            string files = "";
            while (true)
            {
                files = string.Empty;
                foreach (string file in ListaArchivosLocal)
                {
                    files += file;
                    if (!ListaArchivosLocal.Last().Equals(file))
                    {
                        files += ",";
                    }
                }
                string IPLocal = ConfigurationManager.AppSettings.GetValues("IPLocal")[0];
                Paquete p = ProtocolUDPFiles.CrearPaquetes(IPLocal, TcpPort, files);
                UDPClient.Send(p.ByteArray, p.ByteArray.Length, "224.100.0.1", UdpPort);
                while (sw.ElapsedMilliseconds < 30000) { }
                sw.Restart();
            }
        }

        public static void DownloadFile(string NombreArchivo, string ip, string port)
        {
            //Connect to server
            TcpClient client = Connect(ip, Int32.Parse(port));
            string Folder = ConfigurationManager.AppSettings.GetValues("Folder")[0];

            FileStream fs = null;
            try
            {
                //Get a client stream for reading and writing.
                NetworkStream stream = client.GetStream();

                //Creates package request download file
                Paquete Paquete = ProtocolREQDownload.CrearPaquetes(NombreArchivo, 0);

                //Send Request
                stream.Write(Paquete.ByteArray, 0, Paquete.ByteArray.Length);

                //Byte array to read
                int CantBytes = 266;
                byte[] data = new byte[CantBytes];

                //Read file data, first package
                int BytesRead;
                BytesRead = stream.Read(data, 0, CantBytes);

                PaqueteREQ received = new PaqueteREQ(data);
                //string commando = string.Empty;
                //received.ByteArray = data;

                //Process data
                //Get MD5
                string MD5 = received.getMD5();

                //Get Cantidad de paquetes
                int PaquetesTotal = received.GetCantidadPaquetes();

                int PaquetesRecibidos = 0;

                fs = File.Open(Folder + NombreArchivo, FileMode.Create, FileAccess.Write, FileShare.None);
                PaquetesRecibidos = 1; //Primer paquete

                byte[] msg;
                msg = Encoding.ASCII.GetBytes("Se recibio el archivo.");
                stream.Write(msg, 0, msg.Length);

                while (PaquetesRecibidos < PaquetesTotal)
                {
                    while (!stream.DataAvailable && client.Connected) ;
                    if (!client.Connected)
                        throw new SocketException();


                    BytesRead = stream.Read(data, 0, data.Length);
                    received = new PaqueteREQUpload(data);
                    received.ByteArray = data;
                    //commando = received.GetCmd().ToString();

                    // Add information to the file.
                    fs.Write(received.GetDatos(), 0, received.GetDatos().Length);
                    PaquetesRecibidos++;
                    //Last received package

                    msg = Encoding.ASCII.GetBytes("Se recibio el archivo.");
                    stream.Write(msg, 0, msg.Length);
                }

                Console.WriteLine("Servidor actualizado con: " + NombreArchivo);
                //Flush filestream and close file.
                fs.Flush();
                fs.Close();
                ListaArchivosLocal.Add(NombreArchivo);
            }
            catch (SocketException)
            {
                if (fs != null)
                {
                    fs.Flush();
                    fs.Close();
                }
            }
            catch (IOException)
            {
                if (fs != null)
                {
                    fs.Flush();
                    fs.Close();
                }
            }
            catch (Exception)
            {
                if (fs != null)
                {
                    fs.Flush();
                    fs.Close();
                }
            }
        }
    }
}
